メール受信もAPI Gateway+Lambdaで処理してみる
ウィスキー、シガー、パイプをこよなく愛する大栗です。 先日JAWS-UGアーキテクチャ専門支部で、API GatewayとLambdaでメール受信を行うという内容で発表してきたので、紹介します。サンプル実装もしたので解説します。
どうやってメールを受信するのか?
API Gatewayはhttpsのみ体操しているためsmtpは受けられません。AWSにsmtpを受信する機能が無いので外部サービスを活用しています。今回はSendGridを使用しています。
SendGrid Inbound Parse Webhook
SendGridは世界最大規模のメール送信プラットフォームですが、Inbound Parse Webhookというメール受信を行う機能があります。これは、SendGridがメールを受信して、メールの内容をWebhookとしてhttpリクエストしてくれます。
全体構成
SendGridで受信したメールをWebhookでAPI Gatewayへ送り、Lambdaで処理をします。 構成としては、下図の様になります。今回は赤い枠線の中について試してみました。 なお、各サービスのリージョンは全てVirginiaにしています。
設定してみる
Lambdaの設定
まず、Lambdaは以下の内容でFunctionを作成します。 Lambdaを実行するRoleでDynamoDBを操作する権限を付与することを忘れないで下さい。
console.log('Loading function'); var AWS, dynamodb, Region, tableName; tableName = 'mails'; Region = 'us-east-1'; AWS = require('aws-sdk'); AWS.config.update({ region: Region }); dynamodb = new AWS.DynamoDB(); exports.handler = function(event, context) { console.log('Received event:', JSON.stringify(event, null, 2)); var decode = new Buffer(event.body, 'base64').toString(); console.log('Decode Data:', decode); var params; params = { TableName: tableName, Item: { 'request-key' : {"S": event.resourceId}, 'mail-key' : {"S": event.mailKey}, 'mail-secret' : {"S": event.mailSecret}, 'body': {"S": decode } }}; console.log('Put Data:', JSON.stringify(params, null, 2)); dynamodb.putItem(params, function(err, data) { if (err) { console.log('Error:', JSON.stringify(err, null, 2)); context.done(null, err); return; } return context.done(null, 'Post succeeded.'); }); };
これは、入力データのbody
をbase64デコードして、DynamoDBへ保存しています。
DynamoDB
DynamoDBはPrimary KeyをHashにしてrequest-key
という名称にしてテーブル[mails]を作成します。
API Gatewayの設定
今回は/mails
というリソースを作成して、POST
メソッドを作成します。
Method Request
Method RequestではQuery StringとしてMailKey
とMailSecret
を追加します。(ただし、MailKey
とMailSecret
確認するロジックを実装していないため、実際の使用ではロジックの追加が必要です)
以下の理由があるため、API GatewayのAPI Keyを使用していません。
- SendGridのWebhookでは独自のヘッダが使用できないため
- API GatewayのAPI Keyは認可で利用するものでないため
Integration Request
Integration Requestで、作成したLambdaへの接続とデータの整形を行います。POSTされるデータはJSONである必要が有るため、Integration RequestでJSONへ整形します。 基本項目は以下のように設定します。
項目 | 内容 |
---|---|
Integration | type Lambda Function |
Lambda Region | us-east-1 |
Lambda Function | PostMail |
Mapping Templates
ではContent-Typeにapplication/jsonとmultipart/form-dataを設定します。
WebhookのContent-Typeはmultipart/form-dataなのですが、Test実行時のContent-Typeはapplication/jsonであるため、双方設定します。
テンプレートの内容は以下になります。POSTされる内容をBASE64エンコードしてJSONの値としています。
#set($inputParams = $util.base64Encode($input.path('$'))) #set($inputMailKey = $input.params('MailKey')) #set($inputMailSecret = $input.params('MailSecret')) { "stage" : "$context.stage", "requestId" : "$context.requestId", "apiId" : "$context.apiId", "resourcePath" : "$context.resourcePath", "resourceId" : "$context.resourceId", "httpMethod" : "$context.httpMethod", "sourceIp" : "$context.identity.sourceIp", "userAgent" : "$context.identity.userAgent", "accountId" : "$context.identity.accountId", "apiKey" : "$context.identity.apiKey", "caller" : "$context.identity.caller", "user" : "$context.identity.user", "userArn" : "$context.identity.userArn", "mailKey": "$inputMailKey", "mailSecret": "$inputMailSecret", "body": "$inputParams" }
Method RequestとIntegration Requestを設定したらDeploy API
します。Stageには任意の名称を設定してください。
Stageの設定
Stageの設定では、CloudWatch Settingsの各項目を有効にしてLog LevelをINFO
に設定しました。
Route 53の設定
SendGridへメールを送信するために、メール受信用野ドメインのMXレコードをmx.sendgrid.net
へ向ける必要があります。
SendGridの設定
SendGridのダッシュボードから[SETTINGS]-[Inbound Parse]を選択します。
Add Host & URL
をクリックして、HOSTとURLを設定します。
- HOST:受信するメールアドレスのドメイン
- URL:API Gatewayで設定したMethodのURL+Query String
今回は
https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prd/mails?MailKey=abc&MailSecret=1234
のように設定します。
メール送信してみる
設定が環境したら、実際にメールを送信してみます。 以下の様にメールを作成して送信します。
1分程度待つと、DynamoDBへメールの内容が保存されます。
メール本文はSendGridでPOSTされた内容のまま、以下のように保存されます。
--xYzZY Content-Disposition: form-data; name="headers" Received: by mx0034p1XXXX.sendgrid.net with SMTP id wNCQCLXXXX Wed, 25 Sep 2013 09:17:53 +0000 (UTC) Received: from ss06.example.com (ss06.example.com [192.0.2.1]) by mx0034p1xxxx.sendgrid.net (Postfix) with SMTP id 169E2A8XXXX for <[email protected]>; Wed, 25 Sep 2013 09:17:52 +0000 (UTC) Received: from ([192.0.2.1]) (envelope sender: <[email protected]>) by ss06.example.com with Active!gate esmtp server; Wed, 25 Sep 2013 18:17:16 +0900 Received: from ([192.0.2.1]) (envelope sender: <[email protected]>) by mail.example.com with TransWARE esmtp server; Wed, 25 Sep 2013 18:17:16 +0900 Received: by lahh2 with SMTP id h2so9031974XXXX.0 ; Wed, 25 Sep 2013 02:17:13 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=google; h=mime-version:from:date:message-id:subject:to:content-type; bh=774H8YhD73M6Iv12382PjN/VL65678/kQI3o/Dctpus=; b=DStS12345owhGmCs6K5F56oytabcde+vaXrVT/0EUz1dBiMDEf8kHGc5GZo6E98Cpr LxLrstT/5EvWAS12345mIXqYabcdeXnH0bWcs= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20130820; h=x-gm-message-state:mime-version:from:date:message-id:subject:to :content-type; bh=774H8Y123456Iv+uQE2PjN123451/a/kQI3o/Dctpus=; b=PIvHArVabcdewumhfGvhWlnmN8PQy8y+KDW5wwWaVDjDK9YebcdeUCqLC5ay8UB/z SbEZOnGo5xR3OnFoxij4a/fnApMfW3Uv6Msv1abcdekIaKnOgOg3evwoDhfLsO+U6UD3 IK8OZiYAUJRwegsCF oYNdhgf/L94Oj0G+ Ghqw== X-Gm-Message-State: ALoCoHkY0DHEG1GGsM6OkKiQSPLYKJkw4abcdermWSwAKTNA4P/x14Pq3koYrh6MrIqdtVIgLDLlrIWA+Ytq6L1TbM2jDoeN/Tbmw0Rq0+IPMQW3elGp6tnC5qECQ8yCY/12345N4AfPBf1TOAalMKRL74rW4PbZn3z2jqMdrmsoSG2gdpRIFwg= X-Received: by 192.0.2.1 with SMTP id s2mr278326lbr.29.1443172633158; Wed, 25 Sep 2013 02:17:13 -0700 (PDT) X-Received: by 192.0.2.1 with SMTP id s2mr278312lbr.29.1443172632593; Wed, 25 Sep 2013 02:17:12 -0700 (PDT) MIME-Version: 1.0 Received: by 192.0.2.1 with HTTP; Wed, 25 Sep 2013 02:16:53 -0700 (PDT) From: =?UTF-8?B?5aSn5qCX5a6X?= <[email protected]> Date: Wed, 25 Sep 2013 18:16:53 +0900 Message-ID: <CAOgvZW5XuQfYQmYUDNndpCMWoxqY2aUpAKtSJnyzHvq5mLOyJw@mail.example.com> Subject: =?UTF-8?B?44K/44Kk44OI44Or44Gn44GZ?= To: [email protected] Content-Type: multipart/alternative; boundary=582amvu10gk31db1lfy3208ow0df --xYzZY Content-Disposition: form-data; name="dkim" {@example.com : pass} --xYzZY Content-Disposition: form-data; name="to" [email protected] --xYzZY Content-Disposition: form-data; name="html" <div dir="ltr"><div>本文です。</div><div>あいうえお</div><div>かきくけこ</div> </div> --xYzZY Content-Disposition: form-data; name="from" 大栗宗 <[email protected]> --xYzZY Content-Disposition: form-data; name="text" 本文です。 あいうえお かきくけこ --xYzZY Content-Disposition: form-data; name="sender_ip" 223.27.116.84 --xYzZY Content-Disposition: form-data; name="envelope" {"to":["[email protected]"],"from":"[email protected]"} --xYzZY Content-Disposition: form-data; name="attachments" 0 --xYzZY Content-Disposition: form-data; name="subject" タイトルです --xYzZY Content-Disposition: form-data; name="charsets" {"to":"UTF-8","html":"UTF-8","subject":"UTF-8","from":"UTF-8","text":"UTF-8"} --xYzZY Content-Disposition: form-data; name="SPF" pass --xYzZY--
最後に
外部サービスを利用することで、Non EC2アーキテクチャでメールの受信処理が出来る様になりました。今回の構成では障害時のリトライ処理やメール受信の担保を省いているので、実運用に活用する場合はクリティカルな場面を避けたり、リトライや冗長化を図る必要がありますので注意してください。